feat(audio): add USB Audio Host (UAC 1.0) support #3774
Conversation
Add TinyUSB Host Audio class driver supporting UAC 1.0 devices. Features: - Support multiple Audio Streaming (AS) interfaces with independent format storage - Support both IN (Microphone) and OUT (Speaker) endpoints - Per-AS interface format info: channels, sample rate, bit resolution - Support Feature Unit volume control - Support sampling frequency get/set - Add host/audio_host example for STM32F407 discovery board - Support mono-to-stereo conversion for loopback Changes: - Add src/class/audio/audio_host.c and audio_host.h - Register AUDIO driver in usbh.c - Add CFG_TUH_AUDIO macro in tusb_option.h - Add host/audio_host example with CMake and Makefile build support Tested with Jabra USB headset (stereo speaker + mono microphone) on STM32F407 disco.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR introduces initial USB Audio Class (UAC 1.0) Host support to TinyUSB by adding a new host class driver (audio_host.c/.h), wiring it into the host driver registry, and providing a new host-side example application demonstrating enumeration and basic streaming/control requests.
Changes:
- Add a new TUH Audio (UAC 1.0) host class driver and public host API header.
- Integrate the new class driver into the host stack and build systems (CMake + Make).
- Add a new
examples/host/audio_hostexample + README.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| src/tusb.h | Expose TUH audio host API when CFG_TUH_AUDIO is enabled. |
| src/tusb_option.h | Add default CFG_TUH_AUDIO option. |
| src/tinyusb.mk | Compile new audio host class driver. |
| src/host/usbh.c | Register audio host class driver in the host class driver table. |
| src/CMakeLists.txt | Add class/audio/audio_host.c to TinyUSB core sources. |
| src/class/audio/audio_host.h | New TUH audio host public API + callback types. |
| src/class/audio/audio_host.c | New UAC1 host driver implementation (enumeration, set_config, transfers, control requests). |
| examples/host/audio_host/src/tusb_config.h | New example configuration enabling TUH audio. |
| examples/host/audio_host/src/main.c | New host example main loop. |
| examples/host/audio_host/src/audio_app.c | New example “loopback” style audio logic and callbacks. |
| examples/host/audio_host/src/app.h | Example app header. |
| examples/host/audio_host/README.md | Example documentation. |
| examples/host/audio_host/Makefile | Make-based build for the new example. |
| examples/host/audio_host/CMakeLists.txt | CMake-based build for the new example. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| while (tu_desc_in_bounds(p_desc, desc_end)) { | ||
| if (tu_desc_type(p_desc) == TUSB_DESC_INTERFACE) { | ||
| const tusb_desc_interface_t *itf = (const tusb_desc_interface_t *)p_desc; | ||
| if (itf->bInterfaceClass == TUSB_CLASS_AUDIO && itf->bInterfaceSubClass == AUDIO_SUBCLASS_STREAMING) { | ||
| // Found Audio Streaming Interface | ||
| TU_LOG_DRV(" Found AS Interface %u (alt = %u)\r\n", itf->bInterfaceNumber, itf->bAlternateSetting); |
| // If not found, check if this is an AS interface that belongs to a known AC interface | ||
| if (idx >= CFG_TUH_AUDIO_MAX) { | ||
| for (uint8_t i = 0; i < CFG_TUH_AUDIO_MAX; i++) { | ||
| if (_audioh_itf[i].daddr == dev_addr && _audioh_itf[i].as_interface_num == itf_num) { | ||
| // AS interface, already handled by AC interface's set_config | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } |
| // Send SET_INTERFACE for next AS interface if any | ||
| p_audio->as_set_idx++; | ||
| if (p_audio->as_set_idx < p_audio->as_count) { | ||
| uint8_t as_idx = p_audio->as_set_idx; | ||
| uint8_t itf = p_audio->as_interfaces[as_idx]; | ||
| uint8_t alt = p_audio->as_alt_settings[as_idx]; | ||
| if (alt > 0) { | ||
| TU_LOG_DRV("AUDIO Set Interface %u Alt %u (addr = %u)\r\n", itf, alt, xfer->daddr); | ||
| tuh_interface_set(xfer->daddr, itf, alt, audioh_set_interface_complete, idx); | ||
| return; | ||
| } | ||
| } |
| // Send SET_INTERFACE for all AS interfaces with alt_setting > 0 | ||
| if (p_audio->as_count > 0) { | ||
| p_audio->as_set_idx = 0; | ||
| uint8_t itf = p_audio->as_interfaces[0]; | ||
| uint8_t alt = p_audio->as_alt_settings[0]; | ||
| if (alt > 0) { | ||
| TU_LOG_DRV("AUDIO Set Interface %u Alt %u (addr = %u)\r\n", itf, alt, dev_addr); | ||
| tuh_interface_set(dev_addr, itf, alt, audioh_set_interface_complete, idx); | ||
| return true; | ||
| } | ||
| } |
| typedef struct { | ||
| TUH_EPBUF_DEF(epin, CFG_TUH_AUDIO_EPIN_BUFSIZE); | ||
| TUH_EPBUF_DEF(epout, CFG_TUH_AUDIO_EPOUT_BUFSIZE); | ||
| } audioh_epbuf_t; | ||
|
|
||
| static audioh_interface_t _audioh_itf[CFG_TUH_AUDIO_MAX]; | ||
|
|
| uint8_t val_buf[2] = { (uint8_t)(value & 0xFF), (uint8_t)((value >> 8) & 0xFF) }; | ||
|
|
||
| tuh_xfer_t xfer = { | ||
| .daddr = daddr, | ||
| .ep_addr = 0, | ||
| .setup = &request, | ||
| .buffer = val_buf, | ||
| .complete_cb = complete_cb, |
| bool tuh_audio_get_sampling_freq(uint8_t daddr, uint8_t ep_addr, uint32_t *sampling_freq, | ||
| tuh_xfer_cb_t complete_cb, uintptr_t user_data) { | ||
| tusb_control_request_t const request = { |
| uint16_t samples = xferred_bytes / 2; | ||
| mono_to_stereo(audio_rx_buffer, audio_tx_buffer, samples); | ||
| ok = tuh_audio_send(audio_dev_addr, audio_idx, audio_tx_buffer, xferred_bytes * 2); |
| printf(" Sampling frequency set OK, ready for isochronous transfer\r\n"); | ||
| // Set Feature Unit volume to un-mute | ||
| set_feature_unit_volume(); | ||
| // Set OUT sampling frequency then send empty packet to kick-start device | ||
| tuh_audio_set_sampling_freq(audio_dev_addr, audio_ep_out, sampling_freq, NULL, 0); | ||
| audio_ready = true; |
| 4. The example will: | ||
| - Print device information when mounted | ||
| - Set sampling frequency to 48kHz | ||
| - Receive audio samples from the device (IN endpoint) | ||
| - Send test sine wave audio to the device (OUT endpoint) |
|
| target | .text | .rodata | .data | .bss | total | % diff |
|---|---|---|---|---|---|---|
| frdm_k64f/board_test | 860 → 1,004 (+144) | — | — | 32 → 44 (+12) | 892 → 1,048 (+156) | +17.5% |
| ea4088_quickstart/audio_test | 9,540 → 10,468 (+928) | — | — | — | 11,832 → 13,528 (+1,696) | +14.3% |
| ea4088_quickstart/audio_test_multi_rate | 9,752 → 10,680 (+928) | — | — | — | 12,300 → 13,996 (+1,696) | +13.8% |
| ea4088_quickstart/video_capture | 10,780 → 11,660 (+880) | — | — | — | 13,336 → 14,984 (+1,648) | +12.4% |
| ea4088_quickstart/uac2_headset | 10,988 → 11,852 (+864) | — | — | — | 13,776 → 15,408 (+1,632) | +11.8% |
| ea4088_quickstart/uac2_speaker_fb | 11,724 → 12,592 (+868) | — | — | — | 14,768 → 16,404 (+1,636) | +11.1% |
| ea4088_quickstart/webusb_serial | 10,064 → 10,668 (+604) | — | — | — | 12,592 → 13,964 (+1,372) | +10.9% |
| lpcxpresso1769/audio_test | 9,824 → 10,700 (+876) | — | — | 1,804 → 2,188 (+384) | 12,104 → 13,364 (+1,260) | +10.4% |
| ea4088_quickstart/cdc_uac2 | 12,588 → 13,460 (+872) | — | — | — | 15,888 → 17,528 (+1,640) | +10.3% |
| ea4088_quickstart/audio_4_channel_mic | 13,852 → 14,780 (+928) | — | — | — | 16,636 → 18,332 (+1,696) | +10.2% |
…sync control transfer buffer - Stop parsing at first non-Audio interface in audioh_open to avoid claiming unrelated interfaces - Call usbh_driver_set_config_complete for AS and unknown interfaces to allow enumeration to continue - Add global ctrl endpoint buffer to audioh_epbuf_t to fix use-after-return in feature_unit_set - Add sampling_freq NULL check and initialize to 0 in tuh_audio_get_sampling_freq - Change BOARD_TUH_RHPORT from 1 to 0 in audio_host example - Add only.txt with supported MCU/family list for audio_host example
| .bRequest = AUDIO10_CS_REQ_SET_CUR, | ||
| .wValue = tu_u16(control_selector, channel), | ||
| .wIndex = tu_u16(itf_num, unit_id), | ||
| .wLength = 2 |
| .bRequest = AUDIO10_CS_REQ_GET_CUR, | ||
| .wValue = tu_u16(control_selector, channel), | ||
| .wIndex = tu_u16(itf_num, unit_id), | ||
| .wLength = len |
| .bRequest = AUDIO10_CS_REQ_SET_CUR, | ||
| .wValue = tu_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0), // Control Selector = Sampling Freq, Channel = 0 | ||
| .wIndex = tu_u16_low(ep_addr), | ||
| .wLength = 3 | ||
| }; |
| .bRequest = AUDIO10_CS_REQ_GET_CUR, | ||
| .wValue = tu_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0), // Control Selector = Sampling Freq, Channel = 0 | ||
| .wIndex = tu_u16_low(ep_addr), | ||
| .wLength = 3 | ||
| }; |
| static uint8_t freq_buf[3] = {0}; | ||
| tusb_control_request_t const request = { |
| typedef struct { | ||
| TUH_EPBUF_DEF(epin, CFG_TUH_AUDIO_EPIN_BUFSIZE); | ||
| TUH_EPBUF_DEF(epout, CFG_TUH_AUDIO_EPOUT_BUFSIZE); | ||
| TUH_EPBUF_DEF(ctrl, 8); | ||
| } audioh_epbuf_t; |
| bool tuh_audio_set_interface(uint8_t daddr, uint8_t itf_num, uint8_t alt_setting, | ||
| tuh_xfer_cb_t complete_cb, uintptr_t user_data) { | ||
| tusb_control_request_t const request = { | ||
| .bmRequestType_bit = { | ||
| .recipient = TUSB_REQ_RCPT_INTERFACE, | ||
| .type = TUSB_REQ_TYPE_STANDARD, | ||
| .direction = TUSB_DIR_OUT | ||
| }, | ||
| .bRequest = TUSB_REQ_SET_INTERFACE, | ||
| .wValue = alt_setting, | ||
| .wIndex = itf_num, | ||
| .wLength = 0 | ||
| }; | ||
|
|
||
| tuh_xfer_t xfer = { | ||
| .daddr = daddr, | ||
| .ep_addr = 0, | ||
| .setup = &request, | ||
| .buffer = NULL, | ||
| .complete_cb = complete_cb, | ||
| .user_data = user_data | ||
| }; | ||
|
|
||
| return tuh_control_xfer(&xfer); | ||
| } |
| - Print device information when mounted | ||
| - Set sampling frequency to 48kHz | ||
| - Receive audio samples from the device (IN endpoint) | ||
| - Send test sine wave audio to the device (OUT endpoint) |
| // Set OUT sampling frequency then send empty packet to kick-start device | ||
| tuh_audio_set_sampling_freq(audio_dev_addr, audio_ep_out, sampling_freq, NULL, 0); | ||
| audio_ready = true; |
| // Mono microphone, convert to stereo and send to OUT endpoint | ||
| uint16_t samples = xferred_bytes / 2; | ||
| mono_to_stereo(audio_rx_buffer, audio_tx_buffer, samples); | ||
| ok = tuh_audio_send(audio_dev_addr, audio_idx, audio_tx_buffer, xferred_bytes * 2); | ||
| } else { |
Hardware-in-the-loop (HIL) Test Reporthfp.json✅ 39 passed · ❌ 17 failed · ⚪ 0 skipped · blank not run
tinyusb-esp.json✅ 10 passed · ❌ 14 failed · ⚪ 0 skipped · blank not run
|
…audio host - Fix missing tu_htole16() conversions for wValue and wIndex in tuh_audio_set_sampling_freq, tuh_audio_get_sampling_freq, tuh_audio_feature_unit_set, and tuh_audio_feature_unit_get - Fix incorrect wIndex parameter order in feature unit requests (unit_id and itf_num were swapped) - Replace static freq_buf with per-endpoint ctrl buffer in tuh_audio_set_sampling_freq to avoid concurrency issues - Update audio_host README to match actual example behavior
| bool tuh_audio_set_interface(uint8_t daddr, uint8_t itf_num, uint8_t alt_setting, | ||
| tuh_xfer_cb_t complete_cb, uintptr_t user_data) { | ||
| tusb_control_request_t const request = { | ||
| .bmRequestType_bit = { | ||
| .recipient = TUSB_REQ_RCPT_INTERFACE, | ||
| .type = TUSB_REQ_TYPE_STANDARD, | ||
| .direction = TUSB_DIR_OUT | ||
| }, | ||
| .bRequest = TUSB_REQ_SET_INTERFACE, | ||
| .wValue = alt_setting, | ||
| .wIndex = itf_num, | ||
| .wLength = 0 | ||
| }; | ||
|
|
||
| tuh_xfer_t xfer = { | ||
| .daddr = daddr, | ||
| .ep_addr = 0, | ||
| .setup = &request, | ||
| .buffer = NULL, | ||
| .complete_cb = complete_cb, | ||
| .user_data = user_data | ||
| }; | ||
|
|
||
| return tuh_control_xfer(&xfer); | ||
| } |
| // If not found, check if this is an AS interface that belongs to a known AC interface | ||
| if (idx >= CFG_TUH_AUDIO_MAX) { | ||
| for (uint8_t i = 0; i < CFG_TUH_AUDIO_MAX; i++) { | ||
| if (_audioh_itf[i].daddr == dev_addr && _audioh_itf[i].as_interface_num == itf_num) { | ||
| // AS interface: configuration is driven by the AC interface, so just pass through | ||
| usbh_driver_set_config_complete(dev_addr, itf_num); | ||
| return true; | ||
| } | ||
| } | ||
| // Not an Audio interface we own; pass through so enumeration can continue | ||
| usbh_driver_set_config_complete(dev_addr, itf_num); | ||
| return true; | ||
| } |
| typedef struct { | ||
| TUH_EPBUF_DEF(epin, CFG_TUH_AUDIO_EPIN_BUFSIZE); | ||
| TUH_EPBUF_DEF(epout, CFG_TUH_AUDIO_EPOUT_BUFSIZE); | ||
| TUH_EPBUF_DEF(ctrl, 8); | ||
| } audioh_epbuf_t; | ||
|
|
||
| static audioh_interface_t _audioh_itf[CFG_TUH_AUDIO_MAX]; | ||
| static audioh_epbuf_t _audioh_epbuf[CFG_TUH_AUDIO_MAX]; |
| bool tuh_audio_itf_get_info(uint8_t idx, tuh_itf_info_t *info) { | ||
| audioh_interface_t *p_audio = &_audioh_itf[idx]; | ||
| TU_VERIFY(p_audio && info); | ||
|
|
| printf(" Sampling frequency set OK, ready for isochronous transfer\r\n"); | ||
| // Set Feature Unit volume to un-mute | ||
| set_feature_unit_volume(); | ||
| // Set OUT sampling frequency then send empty packet to kick-start device | ||
| tuh_audio_set_sampling_freq(audio_dev_addr, audio_ep_out, sampling_freq, NULL, 0); | ||
| audio_ready = true; |
| if (audio_mic_channels == 1) { | ||
| // Mono microphone, convert to stereo and send to OUT endpoint | ||
| uint16_t samples = xferred_bytes / 2; | ||
| mono_to_stereo(audio_rx_buffer, audio_tx_buffer, samples); | ||
| ok = tuh_audio_send(audio_dev_addr, audio_idx, audio_tx_buffer, xferred_bytes * 2); | ||
| } else { |
| - This example uses isochronous transfers which require precise timing | ||
| - For production applications, synchronize audio transfers with the device's audio clock | ||
| - The example sends a simple sine wave for testing; replace with actual audio data in real applications |
| bool tuh_audio_get_sampling_freq(uint8_t daddr, uint8_t ep_addr, uint32_t *sampling_freq, | ||
| tuh_xfer_cb_t complete_cb, uintptr_t user_data) { | ||
| TU_VERIFY(sampling_freq, false); | ||
| *sampling_freq = 0; | ||
|
|
…nify const style Rename descriptor pointer variables to use desc_ prefix for consistency with audio_device.c: - it -> desc_input_terminal - ot -> desc_output_terminal - fu -> desc_feature_unit - itf -> desc_interface - p_ep -> desc_endpoint Unify const qualifier placement to type_t const * style. Add file header comment describing UAC 1.0 host driver capabilities. Switch license header to SPDX identifier.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 15 changed files in this pull request and generated 9 comments.
Comments suppressed due to low confidence (1)
examples/host/audio_host/README.md:99
- The Notes section says the example sends a "simple sine wave", but the implementation actually loops back received microphone data to the speaker (with optional mono-to-stereo conversion). This is misleading for users trying to understand the example's behavior.
## Notes
- This example uses isochronous transfers which require precise timing
- For production applications, synchronize audio transfers with the device's audio clock
- The example sends a simple sine wave for testing; replace with actual audio data in real applications
| bool audioh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) { | ||
| (void) result; | ||
| const uint8_t idx = get_idx_by_ep_addr(dev_addr, ep_addr); | ||
| TU_VERIFY(idx < CFG_TUH_AUDIO_MAX); | ||
| audioh_interface_t *p_audio = &_audioh_itf[idx]; | ||
|
|
||
| if (ep_addr == p_audio->ep_in) { | ||
| tuh_audio_rx_cb(idx, ep_addr, (uint16_t) xferred_bytes); | ||
| } else if (ep_addr == p_audio->ep_out) { | ||
| tuh_audio_tx_cb(idx, ep_addr, (uint16_t) xferred_bytes); | ||
| } | ||
|
|
||
| return true; | ||
| } |
| case TUSB_DESC_ENDPOINT: { | ||
| const tusb_desc_endpoint_t *desc_endpoint = (const tusb_desc_endpoint_t *)p_desc; | ||
| if (desc_endpoint->bmAttributes.xfer == TUSB_XFER_ISOCHRONOUS) { | ||
| TU_LOG_DRV(" Isochronous EP %02x\r\n", desc_endpoint->bEndpointAddress); | ||
| if (tu_edpt_dir(desc_endpoint->bEndpointAddress) == TUSB_DIR_IN) { | ||
| p_audio->ep_in = desc_endpoint->bEndpointAddress; | ||
| p_audio->ep_in_size = tu_edpt_packet_size(desc_endpoint); | ||
| p_audio->ep_in_interval = desc_endpoint->bInterval; | ||
| desc_cb.desc_ep_in = desc_endpoint; | ||
| // Save to per-AS storage | ||
| if (as_entry_idx < CFG_TUH_AUDIO_MAX_AS) { | ||
| audioh_as_t *as = &p_audio->as[as_entry_idx]; | ||
| as->ep_addr = desc_endpoint->bEndpointAddress; | ||
| as->ep_size = tu_edpt_packet_size(desc_endpoint); | ||
| as->ep_dir = TUSB_DIR_IN; | ||
| as->format_type = tmp_format_type; | ||
| as->num_channels = tmp_num_channels; | ||
| as->sub_frame_size = tmp_sub_frame_size; | ||
| as->bit_resolution = tmp_bit_resolution; | ||
| as->sam_freq_type = tmp_sam_freq_type; | ||
| as->sam_freq_lower = tmp_sam_freq_lower; | ||
| as->sam_freq_upper = tmp_sam_freq_upper; | ||
| for (uint8_t i = 0; i < CFG_TUH_AUDIO_MAX_SAM_FREQ; i++) { | ||
| as->sam_freq[i] = tmp_sam_freq[i]; | ||
| } | ||
| } | ||
| } else { | ||
| p_audio->ep_out = desc_endpoint->bEndpointAddress; | ||
| p_audio->ep_out_size = tu_edpt_packet_size(desc_endpoint); | ||
| p_audio->ep_out_interval = desc_endpoint->bInterval; | ||
| desc_cb.desc_ep_out = desc_endpoint; | ||
| // Save to per-AS storage | ||
| if (as_entry_idx < CFG_TUH_AUDIO_MAX_AS) { | ||
| audioh_as_t *as = &p_audio->as[as_entry_idx]; | ||
| as->ep_addr = desc_endpoint->bEndpointAddress; | ||
| as->ep_size = tu_edpt_packet_size(desc_endpoint); | ||
| as->ep_dir = TUSB_DIR_OUT; | ||
| as->format_type = tmp_format_type; | ||
| as->num_channels = tmp_num_channels; | ||
| as->sub_frame_size = tmp_sub_frame_size; | ||
| as->bit_resolution = tmp_bit_resolution; | ||
| as->sam_freq_type = tmp_sam_freq_type; | ||
| as->sam_freq_lower = tmp_sam_freq_lower; | ||
| as->sam_freq_upper = tmp_sam_freq_upper; | ||
| for (uint8_t i = 0; i < CFG_TUH_AUDIO_MAX_SAM_FREQ; i++) { | ||
| as->sam_freq[i] = tmp_sam_freq[i]; | ||
| } | ||
| } | ||
| } | ||
| TU_ASSERT(tuh_edpt_open(dev_addr, desc_endpoint), 0); | ||
| } |
| bool tuh_audio_set_sampling_freq(uint8_t daddr, uint8_t ep_addr, uint32_t sampling_freq, | ||
| tuh_xfer_cb_t complete_cb, uintptr_t user_data) { | ||
| uint8_t const idx = get_idx_by_ep_addr(daddr, ep_addr); | ||
| TU_VERIFY(idx < CFG_TUH_AUDIO_MAX, false); | ||
| uint8_t* freq_buf = _audioh_epbuf[idx].ctrl; | ||
| tusb_control_request_t const request = { | ||
| .bmRequestType_bit = { | ||
| .recipient = TUSB_REQ_RCPT_ENDPOINT, | ||
| .type = TUSB_REQ_TYPE_CLASS, | ||
| .direction = TUSB_DIR_OUT | ||
| }, | ||
| .bRequest = AUDIO10_CS_REQ_SET_CUR, | ||
| .wValue = tu_htole16(tu_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0)), // Control Selector = Sampling Freq, Channel = 0 | ||
| .wIndex = tu_htole16((uint16_t) ep_addr), | ||
| .wLength = 3 | ||
| }; |
| bool tuh_audio_get_sampling_freq(uint8_t daddr, uint8_t ep_addr, uint32_t *sampling_freq, | ||
| tuh_xfer_cb_t complete_cb, uintptr_t user_data) { | ||
| TU_VERIFY(sampling_freq, false); | ||
| *sampling_freq = 0; | ||
|
|
||
| tusb_control_request_t const request = { | ||
| .bmRequestType_bit = { | ||
| .recipient = TUSB_REQ_RCPT_ENDPOINT, | ||
| .type = TUSB_REQ_TYPE_CLASS, | ||
| .direction = TUSB_DIR_IN | ||
| }, | ||
| .bRequest = AUDIO10_CS_REQ_GET_CUR, | ||
| .wValue = tu_htole16(tu_u16(AUDIO10_EP_CTRL_SAMPLING_FREQ, 0)), // Control Selector = Sampling Freq, Channel = 0 | ||
| .wIndex = tu_htole16((uint16_t) ep_addr), | ||
| .wLength = 3 | ||
| }; |
| bool tuh_audio_feature_unit_set(uint8_t daddr, uint8_t itf_num, uint8_t unit_id, | ||
| uint8_t control_selector, uint8_t channel, | ||
| uint16_t value, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { | ||
| tusb_control_request_t const request = { | ||
| .bmRequestType_bit = { | ||
| .recipient = TUSB_REQ_RCPT_INTERFACE, | ||
| .type = TUSB_REQ_TYPE_CLASS, | ||
| .direction = TUSB_DIR_OUT | ||
| }, | ||
| .bRequest = AUDIO10_CS_REQ_SET_CUR, | ||
| .wValue = tu_htole16(tu_u16(control_selector, channel)), | ||
| .wIndex = tu_htole16(tu_u16(unit_id, itf_num)), | ||
| .wLength = 2 | ||
| }; |
| bool tuh_audio_feature_unit_get(uint8_t daddr, uint8_t itf_num, uint8_t unit_id, | ||
| uint8_t control_selector, uint8_t channel, | ||
| void *buffer, uint8_t len, | ||
| tuh_xfer_cb_t complete_cb, uintptr_t user_data) { | ||
| tusb_control_request_t const request = { | ||
| .bmRequestType_bit = { | ||
| .recipient = TUSB_REQ_RCPT_INTERFACE, | ||
| .type = TUSB_REQ_TYPE_CLASS, | ||
| .direction = TUSB_DIR_IN | ||
| }, | ||
| .bRequest = AUDIO10_CS_REQ_GET_CUR, | ||
| .wValue = tu_htole16(tu_u16(control_selector, channel)), | ||
| .wIndex = tu_htole16(tu_u16(unit_id, itf_num)), | ||
| .wLength = len | ||
| }; |
| bool tuh_audio_set_interface(uint8_t daddr, uint8_t itf_num, uint8_t alt_setting, | ||
| tuh_xfer_cb_t complete_cb, uintptr_t user_data) { | ||
| tusb_control_request_t const request = { | ||
| .bmRequestType_bit = { | ||
| .recipient = TUSB_REQ_RCPT_INTERFACE, | ||
| .type = TUSB_REQ_TYPE_STANDARD, | ||
| .direction = TUSB_DIR_OUT | ||
| }, | ||
| .bRequest = TUSB_REQ_SET_INTERFACE, | ||
| .wValue = alt_setting, | ||
| .wIndex = itf_num, | ||
| .wLength = 0 | ||
| }; | ||
|
|
||
| tuh_xfer_t xfer = { | ||
| .daddr = daddr, | ||
| .ep_addr = 0, | ||
| .setup = &request, | ||
| .buffer = NULL, | ||
| .complete_cb = complete_cb, | ||
| .user_data = user_data | ||
| }; | ||
|
|
||
| return tuh_control_xfer(&xfer); | ||
| } |
| // Callback after IN sampling frequency is set | ||
| static void in_sampling_freq_set_cb(tuh_xfer_t *xfer) { | ||
| if (xfer->result != XFER_RESULT_SUCCESS) { | ||
| printf(" Sampling frequency set FAILED: result=%u\r\n", xfer->result); | ||
| return; | ||
| } | ||
| printf(" Sampling frequency set OK, ready for isochronous transfer\r\n"); | ||
| // Set Feature Unit volume to un-mute | ||
| set_feature_unit_volume(); | ||
| // Set OUT sampling frequency then send empty packet to kick-start device | ||
| tuh_audio_set_sampling_freq(audio_dev_addr, audio_ep_out, sampling_freq, NULL, 0); | ||
| audio_ready = true; | ||
| } |
| if (xferred_bytes > 0 && audio_ep_out != 0 && !audio_tx_busy) { | ||
| bool ok; | ||
| if (audio_mic_channels == 1) { | ||
| // Mono microphone, convert to stereo and send to OUT endpoint | ||
| uint16_t samples = xferred_bytes / 2; | ||
| mono_to_stereo(audio_rx_buffer, audio_tx_buffer, samples); | ||
| ok = tuh_audio_send(audio_dev_addr, audio_idx, audio_tx_buffer, xferred_bytes * 2); | ||
| } else { | ||
| // Stereo microphone, send directly to OUT endpoint | ||
| ok = tuh_audio_send(audio_dev_addr, audio_idx, audio_rx_buffer, xferred_bytes); | ||
| } | ||
|
|
||
| if (ok) { | ||
| audio_tx_busy = true; | ||
| } | ||
| } |
|
Thank you for your work, beside auto reviews, there is a need of some architectural changes: API shaping
Buffering and packet schedulingSimilar to device driver, FIFO should be included in host driver. |
This commit refactors the TUH_AUDIO (USB Audio Host) class driver to
simplify its public API and improve multi-AS (Audio Streaming) interface
support. The changes are focused on three files: the core driver
(audio_host.c/h) and the example application (audio_app.c).
Key changes in src/class/audio/audio_host.h:
- Remove tuh_audio_descriptor_cb_t and tuh_audio_mount_cb_t structures.
The mount callback no longer passes a large descriptor-info struct;
applications query per-AS info via tuh_audio_as_get_info().
- Add tuh_audio_get_dev_addr() and tuh_audio_get_feature_unit_id()
accessors to retrieve device address and feature-unit ID from an
interface index.
- Simplify control-transfer APIs by replacing (daddr, itf_num, unit_id)
parameters with a single idx parameter:
tuh_audio_set_sampling_freq(idx, as_idx, ...)
tuh_audio_get_sampling_freq(idx, as_idx, ...)
tuh_audio_feature_unit_set(idx, control_selector, channel, ...)
tuh_audio_feature_unit_get(idx, control_selector, channel, ...)
- Add synchronous wrapper APIs using TU_API_SYNC macro:
tuh_audio_get_sampling_freq_sync()
tuh_audio_set_sampling_freq_sync()
tuh_audio_feature_unit_set_sync()
tuh_audio_feature_unit_get_sync()
- Update isochronous endpoint APIs to use (idx, as_idx) instead of
(daddr, idx):
tuh_audio_receive(idx, as_idx, buffer, len)
tuh_audio_send(idx, as_idx, buffer, len)
- Remove tuh_audio_descriptor_cb() weak callback.
- Update tuh_audio_mount_cb() signature from mount_cb(param) to no param.
- Update tuh_audio_rx_cb()/tuh_audio_tx_cb() first parameter from idx to
dev_addr for consistency with other class drivers.
Key changes in src/class/audio/audio_host.c:
- Delete tuh_audio_descriptor_cb weak stub.
- Refactor get_idx_by_ep_addr() to iterate all AS interfaces per device
instead of relying on single ep_in/ep_out fields.
- Add audioh_get_ep_addr_by_dir() helper to find an endpoint address by
direction across multiple AS interfaces.
- Simplify audioh_close() cleanup: remove now-removed single-endpoint
fields (ep_in, ep_out) and rely on tu_memclr(p_audio->as, ...).
- Update audioh_xfer_cb() to pass dev_addr (not idx) to rx/tx callbacks,
matching the new callback signature.
- Simplify audioh_open(): remove descriptor-callback emission and the
temporary desc_cb structure; store only ac_itf_num instead of
bInterfaceNumber + iInterface + as_interface_num.
- Rename local descriptor pointers for clarity:
desc_input_terminal (was desc_it)
desc_output_terminal (was desc_ot)
Key changes in examples/host/audio_host/src/audio_app.c:
- Remove now-unnecessary globals: audio_ep_in, audio_ep_out, audio_ac_itf,
audio_feature_unit_id.
- Initialize audio_dev_addr, audio_idx, audiostream_in_idx,
audiostream_out_idx to 0xFF (TUSB_INDEX_INVALID_8) instead of 0.
- Update print_as_interfaces() to use tuh_audio_as_get_count() and
tuh_audio_as_get_info() instead of accessing mount_cb_data.
- Update all callback signatures and API calls to match the new driver API.
|
I've first completed the API design modifications. Please see if it works |
|
As I commented earlier, the class driver should provide a WASAPI/ALSA-like high-level API for audio streaming, while keeping the USB Audio topology private. We can keep the limitation that there is only one logical stream in each direction per instance (AC interface), which is the most common case. If an instance has multiple Audio Streaming interfaces or alternate settings in one direction, their supported configurations can be combined into the corresponding logical stream. The driver keeps the mapping from each configuration to its interface, alternate setting, and endpoint. This follows the general WASAPI exclusive-mode and ALSA Proposed APIOnly discrete configurations need to be supported initially. Continuous sample-rate ranges can be rejected or ignored explicitly. typedef enum {
TUH_AUDIO_FORMAT_S8,
TUH_AUDIO_FORMAT_S16_LE,
TUH_AUDIO_FORMAT_S24_3LE,
TUH_AUDIO_FORMAT_S24_LE,
TUH_AUDIO_FORMAT_S32_LE
} tuh_audio_format_t;
typedef struct {
tuh_audio_format_t format;
uint32_t sample_rate;
uint8_t channels;
} tuh_audio_stream_config_t;
typedef void (*tuh_audio_configure_cb_t)(
uint8_t idx,
tusb_dir_t direction,
tusb_xfer_result_t result,
uintptr_t user_data);Each entry is a complete supported tuple, avoiding invalid combinations between independent format, rate, and channel lists. bool tuh_audio_stream_exists(uint8_t idx, tusb_dir_t direction);
uint8_t tuh_audio_config_count(uint8_t idx, tusb_dir_t direction);
bool tuh_audio_config_get(
uint8_t idx,
tusb_dir_t direction,
uint8_t config_idx,
tuh_audio_stream_config_t* config);
bool tuh_audio_configure(
uint8_t idx,
tusb_dir_t direction,
tuh_audio_stream_config_t const* config,
tuh_audio_configure_cb_t complete_cb,
uintptr_t user_data);
Streaming should be frame-based: uint32_t tuh_audio_write(uint8_t idx, void const* buffer, uint32_t frame_count);
uint32_t tuh_audio_read(uint8_t idx, void* buffer, uint32_t frame_count);
uint32_t tuh_audio_write_available(uint8_t idx);
uint32_t tuh_audio_read_available(uint8_t idx);
bool tuh_audio_start(uint8_t idx, tusb_dir_t direction);
bool tuh_audio_stop(uint8_t idx, tusb_dir_t direction);The class driver should own endpoint selection, FIFO management, transfer replenishment, fractional packet scheduling, and optional feedback processing. Basic microphone exampleThe application selects a supported 48 kHz, mono, 16-bit capture configuration without accessing USB interfaces, alternate settings, or endpoint addresses. static uint8_t mic_idx = TUSB_INDEX_INVALID_8;
static bool mic_ready;
static int16_t mic_samples[48];
static void mic_configured(uint8_t idx, tusb_dir_t direction,
tusb_xfer_result_t result, uintptr_t user_data) {
(void) user_data;
if (direction == TUSB_DIR_IN && result == XFER_RESULT_SUCCESS) {
mic_ready = tuh_audio_start(idx, TUSB_DIR_IN);
}
}
void tuh_audio_mount_cb(uint8_t idx) {
if (!tuh_audio_stream_exists(idx, TUSB_DIR_IN)) {
return;
}
for (uint8_t i = 0; i < tuh_audio_config_count(idx, TUSB_DIR_IN); i++) {
tuh_audio_stream_config_t config;
if (tuh_audio_config_get(idx, TUSB_DIR_IN, i, &config) &&
config.format == TUH_AUDIO_FORMAT_S16_LE &&
config.sample_rate == 48000 &&
config.channels == 1) {
mic_idx = idx;
(void) tuh_audio_configure(idx, TUSB_DIR_IN, &config, mic_configured, 0);
return;
}
}
}
void tuh_audio_umount_cb(uint8_t idx) {
if (idx == mic_idx) {
mic_idx = TUSB_INDEX_INVALID_8;
mic_ready = false;
}
}
void audio_app_task(void) {
const uint32_t frame_count = TU_ARRAY_SIZE(mic_samples);
if (mic_ready && tuh_audio_read_available(mic_idx) >= frame_count) {
(void) tuh_audio_read(mic_idx, mic_samples, frame_count);
// Process 1 ms of 48 kHz mono audio here.
}
}Correctness issues
This smaller API should be enough for a first UAC1 host implementation while leaving room for multiple streams, continuous rates, conversion, and channel mapping later. |
Add TinyUSB Host Audio class driver supporting UAC 1.0 devices.
Features:
Changes:
Tested with Jabra USB headset (stereo speaker + mono microphone) on STM32F407 disco.